﻿using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace Elentok.Common.Helpers
{
  /// <summary>
  /// This class contains various cryptography methods
  /// </summary>
  public static class CryptoHelper
  {
    private static readonly byte[] blank16 = new byte[16];

    #region AES Methods
    /// <summary>
    /// Creates a key from the given password (using SHA256) and creates 
    /// an AesCryptoServiceProvider from the key (using a [0,0,0,...0] initialization vector),
    /// zero padding and CFB (Cipher Feedback) mode.
    /// </summary>
    /// <param name="password"></param>
    /// <returns></returns>
    static AesCryptoServiceProvider CreateAesCryptoServiceProvider(string password)
    {
      AesCryptoServiceProvider crypto = new AesCryptoServiceProvider();

      crypto.Key = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(password));
      crypto.IV = blank16;
      crypto.Mode = CipherMode.CFB;
      crypto.Padding = PaddingMode.Zeros; // PyCrypto doesn't support padding

      return crypto;
    }

    /// <summary>
    /// Encrypts the "source" string and returns a base64 encoded of the encrypted data
    /// </summary>
    /// <param name="source"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    public static string AesEncryptUtf8String(this string source, string password)
    {
      byte[] data = Encoding.UTF8.GetBytes(source);

      using (MemoryStream outStream = new MemoryStream())
      {

        using (CryptoStream stream = new CryptoStream(outStream,
          CreateAesCryptoServiceProvider(password).CreateEncryptor(), CryptoStreamMode.Write))
        {
          stream.Write(data, 0, data.Length);

          // aes encrypts in blocks (128-bit blocks), so if the data doesn't 
          // contain complete 128-bit blocks we add padding
          stream.FlushFinalBlock();

          return Convert.ToBase64String(outStream.ToArray(), Base64FormattingOptions.InsertLineBreaks);
        }
      }
    }

    /// <summary>
    /// Decrypts the base64 string, and returns utf8 encoded decrypted value
    /// </summary>
    /// <param name="base64"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    public static string AesDecryptBase64String(this string base64, string password)
    {
      byte[] data = Convert.FromBase64String(base64);
      using (MemoryStream inStream = new MemoryStream(data))
      {
        using (MemoryStream outStream = new MemoryStream())
        {
          using (CryptoStream stream = new CryptoStream(inStream,
            CreateAesCryptoServiceProvider(password).CreateDecryptor(), CryptoStreamMode.Read))
          {

            byte[] buffer = new byte[1024];
            int bytes = 0;
            while ((bytes = stream.Read(buffer, 0, buffer.Length)) > 0)
            {
              outStream.Write(buffer, 0, bytes);
            }
          }
          return Encoding.UTF8.GetString(outStream.ToArray());
        }
      }
    }

    /// <summary>
    /// Decrypts the binary stream, returns a plain text memory stream
    /// </summary>
    /// <param name="encryptedStream">The stream to decrypt</param>
    /// <param name="password"></param>
    /// <returns></returns>
    public static MemoryStream AesDecrypt(this Stream encryptedStream, string password)
    {
      MemoryStream plainTextStream = new MemoryStream();

      using (CryptoStream cryptoStream = new CryptoStream(encryptedStream,
        CreateAesCryptoServiceProvider(password).CreateDecryptor(), CryptoStreamMode.Read))
      {

        byte[] buffer = new byte[1024];
        int bytes = 0;
        while ((bytes = cryptoStream.Read(buffer, 0, buffer.Length)) > 0)
        {
          plainTextStream.Write(buffer, 0, bytes);
        }

        //return Encoding.UTF8.GetString(outStream.ToArray());
        plainTextStream.Seek(0, SeekOrigin.Begin);
        return plainTextStream;
      }
    }

    /// <summary>
    /// Encrypt the binary stream, returns an encrypted memory stream
    /// </summary>
    /// <param name="plaintextStream">The stream to encrypt</param>
    /// <param name="password"></param>
    /// <returns></returns>
    public static MemoryStream AesEncrypt(this Stream plaintextStream, string password)
    {
      MemoryStream encryptedStream = new MemoryStream();
      using (CryptoStream cryptoStream = new CryptoStream(encryptedStream,
        CreateAesCryptoServiceProvider(password).CreateEncryptor(), CryptoStreamMode.Write))
      {

        byte[] buffer = new byte[128];
        int bytes = 0;
        while ((bytes = plaintextStream.Read(buffer, 0, buffer.Length)) > 0)
        {
          cryptoStream.Write(buffer, 0, bytes);
        }

        //// aes encrypts in blocks (128-bit blocks), so if the data doesn't 
        //// contain complete 128-bit blocks we add padding
        cryptoStream.FlushFinalBlock();

        encryptedStream.Seek(0, SeekOrigin.Begin);
        return encryptedStream;
      }
    }

    /// <summary>
    /// Encrypt the source byte array, returns an encrypted byte array
    /// </summary>
    /// <param name="plaintext">The bytes to encrypt</param>
    /// <param name="password"></param>
    /// <returns></returns>
    public static byte[] AesEncrypt(this byte[] plaintext, string password)
    {
      using (MemoryStream encryptedStream = new MemoryStream())
      {
        using (CryptoStream cryptoStream = new CryptoStream(encryptedStream,
          CreateAesCryptoServiceProvider(password).CreateEncryptor(), CryptoStreamMode.Write))
        {
          cryptoStream.Write(plaintext, 0, plaintext.Length);

          //// aes encrypts in blocks (128-bit blocks), so if the data doesn't 
          //// contain complete 128-bit blocks we add padding
          cryptoStream.FlushFinalBlock();

          encryptedStream.Seek(0, SeekOrigin.Begin);
          return encryptedStream.ToArray();
        }
      }
    }

    /// <summary>
    /// Decrypts the source byte array, returns a plain text byte array
    /// </summary>
    /// <param name="encryptedValue">The bytes to decrypt</param>
    /// <param name="password"></param>
    /// <returns></returns>
    public static byte[] AesDecrypt(this byte[] encryptedValue, string password)
    {
      using (MemoryStream encryptedStream = new MemoryStream(encryptedValue))
      {
        using (MemoryStream plainTextStream = encryptedStream.AesDecrypt(password))
        {
          return plainTextStream.ToArray();
        }
      }
    }
    #endregion

    /// <summary>
    /// Generates an MD5 hash for the given input
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    public static string GenerateMD5(this string value)
    {
      byte[] bytes = Encoding.UTF8.GetBytes(value);
      return bytes.GenerateMD5();
    }

    /// <summary>
    /// Generates an MD5 hash for the given input
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    public static string GenerateMD5(this byte[] value)
    {
      MD5CryptoServiceProvider provider = new MD5CryptoServiceProvider();
      value = provider.ComputeHash(value);
      return value.ToHexString();
    }
  }
}
